Skip to content

Fall back to epoll_wait when epoll_pwait2 is unavailable at runtime - #23825

Merged
bukka merged 1 commit into
php:masterfrom
bukka:poll_epoll_pwait2_fallback
Sep 29, 2026
Merged

bukka merged 1 commit into
php:masterfrom
bukka:poll_epoll_pwait2_fallback

Conversation

@bukka

@bukka bukka commented Sep 21, 2026

Copy link
Copy Markdown
Member

HAVE_EPOLL_PWAIT2 only means the libc exports the wrapper, which glibc does since 2.35 regardless of the running kernel. A PHP built on a kernel with epoll_pwait2 and run on one older than 5.11 gets ENOSYS from every Context::wait(), which makes the epoll backend and thus Backend::Auto unusable. This is the normal shape for Docker images, distro packages and static builds, and it also happens under emulation layers that lack the syscall, as seen in #23478.

Try epoll_pwait2 first and on ENOSYS or ENOTSUP switch the process to epoll_wait, retrying the current call so the caller never sees the error. The flag is process-wide and atomic so ZTS builds do not race on it.

The configure check is unchanged, so the plain link check keeps working when cross-compiling.

Alternative to #23478.

Comment thread main/poll/poll_backend_epoll.c
HAVE_EPOLL_PWAIT2 only tells whether the libc exports the wrapper, which
glibc does since 2.35 regardless of the running kernel. A PHP built on a
kernel with epoll_pwait2 and run on one older than 5.11 gets ENOSYS from
every Context::wait() call, which makes the epoll backend and thus the
Auto backend unusable. The same happens under emulation layers that do
not implement the syscall.

Try epoll_pwait2 first and on ENOSYS or ENOTSUP switch the process to
epoll_wait with a millisecond timeout, retrying the current call so the
failure is never visible to the caller. The flag is process wide since
kernel support is the same for every thread, and it is atomic so the
first concurrent waits in a ZTS build do not race on it.
@bukka
bukka force-pushed the poll_epoll_pwait2_fallback branch from 04d1b82 to 223dd37 Compare September 23, 2026 17:04
@bukka
bukka merged commit 0c6db8c into php:master Sep 29, 2026
18 checks passed
bukka added a commit that referenced this pull request Sep 29, 2026
…23825)

HAVE_EPOLL_PWAIT2 only tells whether the libc exports the wrapper, which
glibc does since 2.35 regardless of the running kernel. A PHP built on a
kernel with epoll_pwait2 and run on one older than 5.11 gets ENOSYS from
every Context::wait() call, which makes the epoll backend and thus the
Auto backend unusable. The same happens under emulation layers that do
not implement the syscall.

Try epoll_pwait2 first and on ENOSYS or ENOTSUP switch the process to
epoll_wait with a millisecond timeout, retrying the current call so the
failure is never visible to the caller. The flag is process wide since
kernel support is the same for every thread, and it is atomic so the
first concurrent waits in a ZTS build do not race on it.
bukka added a commit that referenced this pull request Sep 29, 2026
* PHP-8.6:
  Fall back to epoll_wait when epoll_pwait2 is unavailable at runtime (#23825)
bukka added a commit to bukka/php-src that referenced this pull request Sep 29, 2026
* upstream/master:
  ext/bcmath: Clear the sign of BcMath\Number results that truncate to zero
  Use C11 atomics for epoll_pwait2_available
  Fix phpGH-23980: ZEND_ASSERT violation @ ZEND_INCLUDE_OR_EVAL (include/eval run with a pending exception)
  Fix phpGH-23842: skipLazyInitialization() copies unresolved constant defaults
  Fall back to epoll_wait when epoll_pwait2 is unavailable at runtime (php#23825)
  Fall back to epoll_wait when epoll_pwait2 is unavailable at runtime (php#23825)
  openssl: Fix memory leak by doing early salt validation
  ext/libxml: Keep SimpleXML children alive across reconstruction
  Fix phpGH-23741: pdo_dblib use-after-free of statement error state

# Conflicts:
#	main/poll/poll_backend_epoll.c
zend_atomic_bool_store_ex(&epoll_pwait2_available, false);
}
}
if (UNEXPECTED(!zend_atomic_bool_load_ex(&epoll_pwait2_available)))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not pretty: When pwait2 is unavailable at runtime, folks are paying for true branches, with one of them UNEXPECTED(). I would suggest making it a real else and then goto from line 194 into the else part, similarly to:

goto fallback;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but this is for HAVE_EPOLL_PWAIT2 which means a case when it was compiled with such support but it runs on older kernel later. I was kind of assuming that such case should be UNEXPECTED...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was kind of assuming that such case should be UNEXPECTED...

My understanding is that UNEXPECTED is a very strong hint that the branch will not be taken. But that is not true: Once it is taken once, it will always be taken.

But I'm now realizing: This will likely always perform two atomic loads, even when pwait2 is actually available, since the compiler can't assume that the atomic doesn't change between the two conditions.

An explicit else (+ goto) has clearer semantics and should be faster in all cases. I wouldn't add any expected/unexpected hints, this is something that should be easily handled by the branch predictor.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that UNEXPECTED is a very strong hint that the branch will not be taken. But that is not true: Once it is taken once, it will always be taken.

This is correct. Generally, UNEXPECTED tends to move the code to the cold text section, use it generally only for error-handling code.

But I'm now realizing: This will likely always perform two atomic loads, even when pwait2 is actually available, since the compiler can't assume that the atomic doesn't change between the two conditions.

Also true

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, see #24026

arnaud-lb added a commit to arnaud-lb/php-src that referenced this pull request Sep 30, 2026
* PHP-8.6: (61 commits)
  zend_hrtime: use CLOCK_MONOTONIC instead of CLOCK_MONOTONIC_RAW (php#23790)
  sapi/cli: Fix built-in server truncating responses after a partial write
  Fix phpGH-24006: Skip gh18431.phpt when libzip lacks progress callbacks (php#24009)
  Updated to version 2026.5 (2026e)
  Fix phpGH-23896: Assertion failure in zend_call_function() after a throwing deprecation
  NEWS
  ext/gd: fix undefined behavior with GIFs with problematic LZW compression data
  Document missing deprecation entries for PHP 8.6 (php#23972)
  ext/tidy: Reject tidyNode use after the document is reparsed
  ext/bcmath: Clear the sign of BcMath\Number results that truncate to zero
  Fix phpGH-23980: ZEND_ASSERT violation @ ZEND_INCLUDE_OR_EVAL (include/eval run with a pending exception)
  Fix phpGH-23842: skipLazyInitialization() copies unresolved constant defaults
  Fall back to epoll_wait when epoll_pwait2 is unavailable at runtime (php#23825)
  openssl: Fix memory leak by doing early salt validation
  ext/libxml: Keep SimpleXML children alive across reconstruction
  Fix phpGH-23741: pdo_dblib use-after-free of statement error state
  NEWS
  Fix property hook escape analysis causing misoptimization
  Fix __isset escape analysis causing misoptimization
  Fix memory leak when closing a statement on a killed connection
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants